package graph.dependencies;

import exceptions.DependencyException;
import graph.InterfaceSearcher;
import graph.adapters.IAdapter;
import graph.adapters.IJavaElementToIPackageFragmentCastAdapter;
import graph.filters.IFilter;
import graph.filters.IJavaElementElementTypeFilter;
import graph.groupers.IGrouper;
import graph.groupers.MethodInvocationBySourceMethodGrouper;
import graph.searchers.IJavaElementTreeClaimer;
import graph.searchers.Searcher;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import tasks.CreateInterfaceTask;
import tasks.ITask;
import tasks.TaskPool;

/**
 * Works with MethodInvocations. Schedules a task to extract an interface
 * from method calls in code. 
 * @author Hornman
 *
 */
public class MethodInvocationDependency2 implements IDependency {
	
	private MethodInvocation _methodInvocation = null;
	
	private static List<MethodInvocation> _methodInvocationDependencies = new LinkedList<MethodInvocation>();
	
	private static List<List<MethodInvocation>> _groupedInvocations = new LinkedList<List<MethodInvocation>>();
	
	private static IGrouper<IMethodBinding,MethodInvocation> _grouper = new MethodInvocationBySourceMethodGrouper();
	
	public static void clearInstances() {
		_methodInvocationDependencies.clear();
		_groupedInvocations.clear();
	}
	
	/**
	 * MethodInvocationDependency2 Constructor.
	 * @param methodInvocation - MethodInvocation node to be analyzed
	 */
	public MethodInvocationDependency2(MethodInvocation methodInvocation) {
		_methodInvocation=methodInvocation;
		_methodInvocationDependencies.add(_methodInvocation);
	}
	
	/**
	 * Searches IPackageFragment that includes child element <b>child</b>
	 * of type <b>elementType</b>.  
	 * @param elementType - int type of node
	 * @param child - IJavaElement node
	 * @return - IPackageFragment
	 * @throws DependencyException 
	 */
	private IPackageFragment getParentNode(IJavaElement child, int elementType ) throws DependencyException {
		Searcher<Integer, IPackageFragment, IJavaElement> s = new Searcher<Integer, IPackageFragment, IJavaElement>();
		Iterator<IJavaElement> treeClaimer = new IJavaElementTreeClaimer(child);
		IFilter<Integer, IJavaElement> elementTypeFilter = new IJavaElementElementTypeFilter();
		elementTypeFilter.setEtalon(elementType);
		IAdapter<IPackageFragment, IJavaElement> caster = new IJavaElementToIPackageFragmentCastAdapter();
		List<IPackageFragment> searchResults = s.search(treeClaimer, elementTypeFilter, caster);
		if (searchResults.size()==1)
			return searchResults.get(0);
		else
			throw new DependencyException("MethodInvocationDependency>Not found parent type node for "+_methodInvocation);
	}

	/**
	 * Checks whether MethodInvocationDependency2 is legal from point of
	 * view of SOLID principles.
	 * @return - boolean. True if MethodInvocationDependency2 is correct.
	 * @throws DependencyException 
	 */
	@Override 
	public boolean isLegal() throws DependencyException {
		try {
			IMethodBinding iMethodBinding = _methodInvocation.resolveMethodBinding();
			if (iMethodBinding==null)
				return true;
			IPackageFragment iPackageFragment = getParentNode(iMethodBinding.getJavaElement(),IJavaElement.PACKAGE_FRAGMENT);
			if (iPackageFragment==null)
				return true;
			if (_methodInvocation.resolveTypeBinding().isInterface() ||
					(_methodInvocation.resolveTypeBinding().getModifiers() & Modifier.STATIC)==1) {
				return true;
			}
			try {
				if (iPackageFragment.getKind() == IPackageFragmentRoot.K_SOURCE)
					return false;
			} catch (JavaModelException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			return true;
		} catch (DependencyException e) {
			throw e;
		} catch (Exception e) {
			throw new DependencyException("MethodInvocationDependency>Error while checking if invocation "+_methodInvocation+" is legal");
		}
	}

	/**
	 * Description of MethodInvocationDependency2 principles violation.
	 * @return - String. If MethodInvocationDependency2 is correct, 
	 * return value is null.
	 * @throws DependencyException 
	 */
	@Override
	public String getDescription() throws DependencyException {
		if (isLegal())
			return null;
		return "Method call: "+_methodInvocation.toString()+" happens on a class instance, not on an interface";
	}
	
	/**
	 * Removes MethodInvocations from input collection that correspond
	 * to duplicate IMethodBindings.
	 * @param groupedInvocations - List<MethodInvocation>. Input
	 * collection of MethodInvocations.
	 */
	private void filterDuplicateMethods(List<MethodInvocation> groupedInvocations) {
		List<IMethodBinding> methods = new LinkedList<IMethodBinding>();
		List<MethodInvocation> duplicate = new LinkedList<MethodInvocation>();
		for (MethodInvocation mI: groupedInvocations) {
			boolean bindingAlreadyExists = false;
			for (IMethodBinding iMB: methods)
				if (iMB.isEqualTo(mI.resolveMethodBinding()))
					bindingAlreadyExists = true;
			if (bindingAlreadyExists)
				duplicate.add(mI);
			else
				methods.add(mI.resolveMethodBinding());
		}
		groupedInvocations.removeAll(duplicate);
	}
	
	/**
	 * Checks whether in enclosing Project there is an Interface with
	 * methods that have similar signatures.
	 * @param invocationsOfIncludedMethods - List<MethodInvocation>. 
	 * Collection of MethodInvocations tobe searched.
	 * @return - boolean. True, if such an interface exists.
	 */
	private boolean checkExistanceOfInterface(List<MethodInvocation> invocationsOfIncludedMethods) {
		List<IMethodBinding> methodBindings = new LinkedList<IMethodBinding>();
		for (MethodInvocation methodInvocation:invocationsOfIncludedMethods) 
			methodBindings.add(methodInvocation.resolveMethodBinding());

		//ITypeBinding foundInterface = SearcherOfAlreadyExistingInterfaceOfClassWithSpecifiedMethods.getInterface(
		//		invocationsOfIncludedMethods.get(0).resolveMethodBinding().getDeclaringClass(), methodBindings);
		IType foundInterface = null;
		try {
			foundInterface = InterfaceSearcher.getInterface(methodBindings, true);
		} catch (JavaModelException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		if (foundInterface!=null)
			return true;
		return false;
	}
	
	/**
	 * Schedules a task that will fix the wrong dependency. If
	 * dependency isLegal, does nothing.
	 * @throws DependencyException 
	 */
	@Override
	public void scheduleFixTask() throws DependencyException {
		try {
			if (isLegal())
				return;
			
			List<MethodInvocation> groupedInvocations = new LinkedList<MethodInvocation>();
			for (MethodInvocation mI: _methodInvocationDependencies) {
						
				if (_grouper.getGroup(mI).isEqualTo(_grouper.getGroup(_methodInvocation))
					&& _methodInvocation.resolveMethodBinding().getDeclaringClass().isEqualTo(mI.resolveMethodBinding().getDeclaringClass())
					)
					groupedInvocations.add(mI);
			}
			_methodInvocationDependencies.removeAll(groupedInvocations);
			
			filterDuplicateMethods(groupedInvocations);
			
			for (List<MethodInvocation> interfaceGroup: _groupedInvocations) {
				if (groupedInvocationsAreEqual(groupedInvocations,interfaceGroup))
					return;
			}
			
			_groupedInvocations.add(groupedInvocations);
	
			if (!groupedInvocations.isEmpty() && !checkExistanceOfInterface(groupedInvocations)) {
				ITask newTask = new CreateInterfaceTask(groupedInvocations);
				TaskPool.getInstance().add(newTask);
			}
		} catch (DependencyException e) {
			throw e;
		} catch (Exception e) {
			throw new DependencyException("MethodInvocationDependency>Error while scheduling fix task for invocation "+_methodInvocation);
		}
	}

	private boolean groupedInvocationsAreEqual(
			List<MethodInvocation> groupedInvocations,
			List<MethodInvocation> interfaceGroup) {
		if (groupedInvocations.size()!=interfaceGroup.size())
			return false;
		for (MethodInvocation groupedInvocation:groupedInvocations) {
			boolean currFound = false;
			for (MethodInvocation interf:interfaceGroup)
				if (interf.resolveMethodBinding().isEqualTo(groupedInvocation.resolveMethodBinding())) {
					currFound = true;
					break;
				}
			if (!currFound)
				return true;
		}
		return false;
	}
	
	
}
